package repostiory

import (
	"context"
	"gitee.com/chicken-c/go-learn/webook/internal/domain"
	"gitee.com/chicken-c/go-learn/webook/internal/repostiory/cache"
	"gitee.com/chicken-c/go-learn/webook/internal/repostiory/dao"
	"gitee.com/chicken-c/go-learn/webook/pkg/logger"
	"github.com/ecodeclub/ekit/slice"
	"github.com/gin-gonic/gin"
	"time"
)

type ArticleRepostiory interface {
	Create(ctx context.Context, article domain.Article) (int64, error)
	Update(ctx context.Context, art domain.Article) error
	Save(ctx *gin.Context, art domain.Article) (int64, error)
	SyncStatus(ctx *gin.Context, artId int64, authorId int64) error
	GetByAuthorId(ctx *gin.Context, offset int64, limit int64, authorId int64) ([]domain.Article, error)
	GetByArticleId(ctx *gin.Context, artId int64) (domain.Article, error)
	GetPubByartId(ctx *gin.Context, id int64) (domain.Article, error)
	ListPub(ctx context.Context, start time.Time, offset int64, limit int64) ([]domain.Article, error)
}

type CachedArticleRepostiory struct {
	dao   dao.ArticleDAO
	cache cache.ArticleCache
	//User Repository
	svc UserRepository

	l logger.Logger
}

func (c *CachedArticleRepostiory) ListPub(ctx context.Context, start time.Time, offset int64, limit int64) ([]domain.Article, error) {
	arts, err := c.dao.ListPub(ctx, start, offset, limit)
	if err != nil {
		return nil, err
	}

	return slice.Map(arts, func(idx int, art dao.PublishedArticle) domain.Article {
		return c.entityToDomain(dao.Article{
			Id:       art.Id,
			Title:    art.Title,
			Content:  art.Content,
			AuthorId: art.AuthorId,
			Status:   art.Status,
			Ctime:    art.Ctime,
			Utime:    art.Utime,
		})
	}), nil
}

func NewCachedArticleRepostiory(dao dao.ArticleDAO, svc UserRepository, cache cache.ArticleCache, l logger.Logger) ArticleRepostiory {
	return &CachedArticleRepostiory{
		dao:   dao,
		svc:   svc,
		cache: cache,
		l:     l,
	}
}

func (c *CachedArticleRepostiory) GetPubByartId(ctx *gin.Context, id int64) (domain.Article, error) {
	//先查cache
	a, err := c.cache.GetPub(ctx, id)
	if err == nil {
		return a, err
	}

	ae, err := c.dao.GetPubById(ctx, id)
	if err != nil {
		return domain.Article{}, err
	}

	//操作UserRepository获取用户信息
	user, err := c.svc.FindById(ctx, ae.AuthorId)
	if err != nil {
		return domain.Article{}, err
	}

	art := domain.Article{
		Id:      ae.Id,
		Title:   ae.Title,
		Content: ae.Content,
		Author: domain.Author{
			Id:       ae.AuthorId,
			Nickname: user.Nickname,
		},
		Status: domain.ArticleStatus(ae.Status),
		Ctime:  time.UnixMilli(ae.Ctime),
		Utime:  time.UnixMilli(ae.Utime),
	}

	//回种到db
	err = c.cache.SetPub(ctx, art)
	if err != nil {
		c.l.Warn("回写cache失败", logger.Field{
			Key: "err",
			Val: err,
		})
	}

	return art, nil
}

func (c *CachedArticleRepostiory) GetByArticleId(ctx *gin.Context, artId int64) (domain.Article, error) {
	//先查cache
	a, err := c.cache.Get(ctx, artId)
	if err == nil {
		return a, nil
	}

	ae, err := c.dao.GetById(ctx, artId)
	if err != nil {
		return domain.Article{}, err
	}
	//不需要回种到cache,因为创作者的缓存收益不高
	return c.entityToDomain(ae), nil
}

func (c *CachedArticleRepostiory) GetByAuthorId(ctx *gin.Context, offset int64, limit int64, authorId int64) ([]domain.Article, error) {
	//先查cache,这里我们只有查询第一页才去cache
	if offset == 0 && limit <= 100 {
		art, err := c.cache.GetFirstPage(ctx, authorId)
		if err == nil {
			//直接命中cache
			return art, nil
		}
	}

	//这里才是查db
	artsE, err := c.dao.GetByAuthorId(ctx, offset, limit, authorId)
	if err != nil {
		return nil, err
	}

	//toDomain
	arts := make([]domain.Article, 0, len(artsE))
	for _, ae := range artsE {
		arts = append(arts, c.entityToDomain(ae))
	}

	//让控制者决定是否异步
	go func() {
		//cache预加载list的第一篇文章
		err := c.preCache(ctx, arts)
		if err != nil {
			//这个错误不需要特别关心，缓存预写失败影响不大
			c.l.Warn("缓存预加载失败", logger.Field{
				Key: "err",
				Val: err,
			})
		}
	}()

	//回种到cache,这里也可以异步
	if offset == 0 && offset <= 100 {
		err = c.cache.SetFirstPage(ctx, authorId, arts)
		if err != nil {
			//redis发生错误，这里我们打个日志就行，回种出错不是很大的问题
			//和之前的user的profile缓存不一样，我们这里只缓存了第一页。
			c.l.Warn("redis发生错误", logger.Field{
				Key: "err",
				Val: err,
			})
		}
	}

	return arts, nil
}

func (c *CachedArticleRepostiory) preCache(ctx *gin.Context, arts []domain.Article) error {
	return c.cache.Set(ctx, arts)
}

func (c *CachedArticleRepostiory) SyncStatus(ctx *gin.Context, artId int64, authorId int64) error {
	return c.dao.SyncStatus(ctx, artId, authorId)
}

func (c *CachedArticleRepostiory) Save(ctx *gin.Context, art domain.Article) (int64, error) {
	id, err := c.dao.Sync(ctx, c.domainToEntity(art))
	if err != nil {
		return id, err
	}

	//当文章发布之后，我们预测马上就会有用户访问，所以我们这里直接缓存这篇文章
	go func() {
		err = c.cache.DelFirstPage(ctx, art.Author.Id)
		if err != nil {
			//db更新了,cache删除失败，数据不一致问题
			c.l.Warn("作者文章第一页缓存删除失败", logger.Field{
				Key: "err",
				Val: err,
			}, logger.Field{
				Key: "author_id",
				Val: art.Author.Id,
			})
		}

		//获取用户信息
		user, err := c.svc.FindById(ctx, art.Author.Id)
		if err != nil {
			c.l.Warn("获取用户信息失败")
		}
		art.Author = domain.Author{
			Id:       user.Id,
			Nickname: user.Nickname,
		}

		//设置缓存，这里的过期时间可以根据作者是不是大V来决定，可以给大V更长的缓存时间
		err = c.cache.SetPub(ctx, art)
		if err != nil {
			c.l.Warn("提前设置缓存失败", logger.Field{
				Key: "authorId",
				Val: art.Author.Id,
			})
		}
	}()

	return id, err
}

func (c *CachedArticleRepostiory) Create(ctx context.Context, art domain.Article) (int64, error) {
	id, err := c.dao.Insert(ctx, c.domainToEntity(art))
	if err != nil {
		return id, err
	}

	err = c.cache.DelFirstPage(ctx, art.Author.Id)
	if err != nil {
		//db更新了,cache删除失败，数据不一致问题
		c.l.Warn("缓存删除失败", logger.Field{
			Key: "err",
			Val: err,
		})
	}

	return id, err
}

func (c *CachedArticleRepostiory) Update(ctx context.Context, art domain.Article) error {
	err := c.dao.Update(ctx, c.domainToEntity(art))
	if err != nil {
		return err
	}

	err = c.cache.DelFirstPage(ctx, art.Author.Id)
	if err != nil {
		//db更新了,cache删除失败，数据不一致问题
		c.l.Warn("缓存删除失败", logger.Field{
			Key: "err",
			Val: err,
		})
	}

	return err
}

func (c *CachedArticleRepostiory) entityToDomain(ae dao.Article) domain.Article {
	return domain.Article{
		Id:      ae.Id,
		Title:   ae.Title,
		Content: ae.Content,
		Author: domain.Author{
			Id: ae.AuthorId,
		},
		Status: domain.ArticleStatus(ae.Status),
		Ctime:  time.UnixMilli(ae.Ctime),
		Utime:  time.UnixMilli(ae.Utime),
	}
}

func (c *CachedArticleRepostiory) domainToEntity(art domain.Article) dao.Article {
	return dao.Article{
		Id:       art.Id,
		Title:    art.Title,
		Content:  art.Content,
		AuthorId: art.Author.Id,
		Status:   art.Status.Uint8(),
		Ctime:    art.Ctime.UnixMilli(),
		Utime:    art.Utime.UnixMilli(),
	}
}
